/**
 * Advanced Software Engineering (Monica Farrow)
 * ParcelUncolleted class
 * 
 * Some of the methods in this class have been based on lecture notes provided by
 * Monica Farrow as a part of Software Engineering courses at Heriot-Watt University.
 */

package depotModels;

import java.io.InputStream;
import java.util.*;

import depot.Parcel;
import depotAdmin.*;
import depotInterfaces.*;

/**
 * ParcelUncolleted class - contains collection of parcels awaiting processing
 * and methods to manipulate the collection
 * @author Ionut Iordache, Nikky Afunugo, Dave Bond
 */
public class ParcelUncollected implements DepotSubject {

	// holds parcels awaiting processing
	private Map<String, Parcel> parcelList = new HashMap<String, Parcel>();
	// holds list of observers
	private List<DepotObserver> registeredObservers = new LinkedList<DepotObserver>();
	// holds final list of uncollected parcels
	private ArrayList<Parcel> finalUncollected;
	// indicates whether parcelList initialised
	private Boolean parcelFileLoaded = false; 
	// stores log entries
	private String eventLogEntry = "";  


	/** 
	 * constructor for ParcelUncolleted
	 * creates an empty HashMap for object type Parcel
	 */ 
	public ParcelUncollected() { 
		parcelList = new HashMap<String, Parcel>();
	} 


	/** 
	 * converts the HashMap into an ArrayList for production of the final report
	 * @return	the ArrayList<Parcel>
	 */ 
	public ArrayList<Parcel> hashMapToArray() {
		ArrayList<Parcel> list = new ArrayList<Parcel>(parcelList.values());
		return list;
	}

	/** 
	 * gets a randomly selected parcel from parcelList
	 * used in creation of randomly generated customer objects
	 * @return	the parcel
	 */ 
	public Parcel getRandomPcl() {
		// put parcelIDs into ArrayList
		ArrayList<String> list = new ArrayList<String>(parcelList.keySet());
		// select random parcelID
		String key = list.get(DepotUtils.getRandomInteger(0, list.size()-1));
		// get the parcel
		Parcel pcl = getParcelByID(key,"checkForDup");
		// remove the parcel from parcelList
		removeParcel(key);

		return pcl;
	}


	/** 
	 * gets a parcel from parcelList using the parcelID in the parameter
	 * @param id   the parcelID
	 * @return	the parcel
	 */ 
	public Parcel getPcl(String id) {
		Parcel pcl = parcelList.get(id);
		return pcl;
	}


	/**
	 * adds the parcel in the parameter to parcelList
	 * @param id   the ID of the parcel
	 * @param pcl   the parcel
	 * @param end   whether it's the end of the day (tidying up)
	 */
	public void addParcelToCollection(String id, Parcel pcl, Boolean end) {
		parcelList.put(id, pcl);

		// start eventLog entry
		eventLogEntry += "<Pcl> Parcel " + pcl.getParcelID();

		if(! end) {
			// update variables holding data for final report
			DepotUtils.incrementTotParcels();
			DepotUtils.incrementTotWeight(pcl.getWeight());
			DepotUtils.incrementTotDays(pcl.getDays());

			eventLogEntry += " added";
		}
		else {
			eventLogEntry += " returned";
		}

		// notify GUI
		notifyObservers("add " + id);

		// finish eventLog entry
		eventLogEntry += " to ParcelUncollected\r\n";
		writeToLog("eventLog.txt",eventLogEntry,"append");
	}


	/** 
	 * removes a Parcel from parcelList
	 * @param id   the ID of the parcel
	 * @return	Boolean indicating success or failure
	 */ 
	public Boolean removeParcel(String id) {
		Boolean isOK = false;
		if(parcelList.remove(id) != null) {

			isOK = true;

			// notify GUI
			notifyObservers("rem " + id);

			// create eventLog entry
			eventLogEntry = "<Pcl> Parcel " + id + " removed from ParcelUncollected\r\n";
			writeToLog("eventLog.txt",eventLogEntry,"append");
		}
		return isOK;
	}


	/** 
	 * takes ID and  returns the Parcel object with matching ID
	 * @param id	the ID of the parcel
	 * @param mode  information about method usage
	 * @return the Parcel or null
	 */
	public synchronized Parcel getParcelByID(String id, String mode) {

		if (mode.equalsIgnoreCase("getCustPcl")) {
			while(! parcelFileLoaded) {
				try {
					wait();
				} catch (InterruptedException ie) {
				}
			}
		}

		if (parcelList.containsKey(id)) {
			Parcel pcl = parcelList.get(id);
			return pcl;
		}
		return null;
	}

	/**
	 * checks state of parcelFileLoaded variable
	 * @return Boolean parcelFileLoaded 
	 */
	public Boolean getParcelFileLoaded() {
		return parcelFileLoaded;
	}


	/**
	 * sets state of parcelFileLoaded variable to that contained in parameter
	 * @param parcelFileLoaded   Boolean object
	 */
	public void setParcelFileLoaded(Boolean parcelFileLoaded) {
		this.parcelFileLoaded = parcelFileLoaded;
	}


	/** Takes filename as parameter
	 * reads file line-by-line
	 * passes each line to separate method for processing
	 * Blank lines are skipped
	 * @param filename the name of the input file
	 */
	public synchronized void processInputFile(String filename) {
		int lineNum = 0;

		// read file into inputstream (so can be included in jar)
		InputStream is = getClass().getResourceAsStream(filename);

		Scanner scanner = null;
		scanner = new Scanner(is);

		while (scanner.hasNextLine()) {
			try {
				lineNum++;

				// read first line and process it
				String inputLine = scanner.nextLine(); 

				if (inputLine.length() != 0) { // ignore blank lines
					processLine(inputLine,lineNum);
				}
			} 


			// this catches invalid conversion from String to an integer or double
			// and produces an entry for the inputErrorLog file
			catch (NumberFormatException nfe) {
				eventLogEntry += "<ERR> parcels.csv, ";
				eventLogEntry += "line "+lineNum+": illegal integer or double conversion\r\n";
				writeToLog("eventLog.txt",eventLogEntry,"append");
			}

			// this catches missing items and produces an entry for the inputErrorLog file
			catch (ArrayIndexOutOfBoundsException nfe) {
				eventLogEntry += "<ERR> parcels.csv, ";
				eventLogEntry += "line "+lineNum+": not enough elements\r\n";
				writeToLog("eventLog.txt",eventLogEntry,"append");
			}

			// this catches (mainly) invalid format errors passed from constructors
			// and produces an entry for the inputErrorLog file
			catch (IllegalArgumentException iae) {
				eventLogEntry += "<ERR> parcels.csv, ";
				eventLogEntry += "line "+lineNum+": "+iae.getMessage()+"\r\n";
				writeToLog("eventLog.txt",eventLogEntry,"append");
			}

			// this catches duplicate IDs and produces an entry for the inputErrorLog file
			catch (DuplicateIDException die) {
				eventLogEntry += "<ERR> parcels.csv, ";
				eventLogEntry += "line "+lineNum+": "+die.getMessage()+"\r\n";
				writeToLog("eventLog.txt",eventLogEntry,"append");
			}

		}

		scanner.close();

		// set areParcels to true and notify waiting threads
		setParcelFileLoaded(true);
		notifyAll();

	}


	/**
	 * Processes each line from processInputFIle, extracts data,
	 * creates Parcel object and adds to parcelList.
	 * Each line contains: ID, length, height, width, weight, days
	 * @param line   the line to be processed
	 * @param lineNum   the line number
	 * @return String containing eventLog entry
	 * @throws IllegalArgumentException, DuplicateIDException, ArrayIndexOutOfBoundsException, NumberFormatException  
	 */
	public void processLine(String line, int lineNum) throws IllegalArgumentException, DuplicateIDException, ArrayIndexOutOfBoundsException, NumberFormatException {

		try {
			Thread.sleep(DepotUtils.getInitAndTidyUpSpeed());
		} catch (InterruptedException e) {
		} 
		String details [] = line.split(",");

		// take id
		String id = details[0];
		id = id.trim();

		// checks parcelList for duplicate ID
		Parcel pcl99 = getParcelByID(id, "checkForDup");
		if(pcl99 != null) {
			throw new DuplicateIDException("ParcelID "+id+" already exists in uncollected parcels list");
		}

		// creates int [] and passes in dimensions, converted from String
		int dims[] = new int[3];
		for (int i=0; i < 3; i++) {
			dims[i] = Integer.parseInt(details[i+1]);
		} 

		// take weight
		double weight = Double.parseDouble(details[4]);

		// take days
		int days = Integer.parseInt(details[5]);

		// creates new Parcel object and adds to parcelList
		Parcel pcl = new Parcel(id, dims, weight, days);
		addParcelToCollection(id, pcl, DepotUtils.getPclLoopFinished());

	}

	/**
	 * adds a number of randomly generated parcels
	 * at regular intervals
	 * @return String containing eventLog entry
	 */
	public void addRandomParcels() {

		while (! DepotUtils.getEndOfDay()) {
			try { // delay to make visible in GUI

				// implement random delay based on current speed value
				long baseDelay = DepotUtils.getParcelAddSpeed();
				// generate random modifier between 0.5 & 1.5
				Double randModifier = DepotUtils.getRandomDouble(0.5, 1.5);
				// apply modifier to base value and convert back to long
				Double convertedBase = baseDelay * randModifier;
				long thisDelay = convertedBase.longValue();

				Thread.sleep(thisDelay);

			} catch (InterruptedException e) {
			} 
			try {
				//generate parcel
				Parcel	pcl = DepotUtils.getRandPcl();

				// checks parcelList for duplicate ID
				Parcel pcl99 = getParcelByID(pcl.getParcelID(), "checkForDup");
				if(pcl99 != null) {
					throw new DuplicateIDException("ParcelID "+ pcl.getParcelID() +" already exists in uncollected parcels list");
				}

				// add parcel to ParcelUncollected
				addParcelToCollection(pcl.getParcelID(), pcl, DepotUtils.getPclLoopFinished());

			}

			// this catches (mainly) invalid format errors passed from constructors
			// and produces an entry for the inputErrorLog file
			catch (IllegalArgumentException iae) {
				eventLogEntry += "<ERR> Random parcel: " + iae.getMessage() + "\r\n";
				writeToLog("eventLog.txt",eventLogEntry,"append");
			}

			// this catches duplicate IDs and produces an entry for the inputErrorLog file
			catch (DuplicateIDException die) {
				eventLogEntry += "<ERR> Random parcel: " + die.getMessage() + "\r\n";
				writeToLog("eventLog.txt",eventLogEntry,"append");
			}

		}

	}

	/**
	 *  returns a table with one line per parcel in uncollectedParcels
	 *  @param finalUncollected  an ArrayList of parcels
	 * @return  table with one line per parcel
	 */
	// forms part of overall report
	public String produceUncollectedTable(ArrayList<Parcel> finalUncollected) {

		String table = "";

		if (finalUncollected.size() == 0) { // if ParcelList is empty
			table = "ALL PARCELS HAVE BEEN COLLECTED";
		}

		else {
			for (Parcel pcl: finalUncollected) {
				table += " " + String.format("%-27s",pcl.getParcelID());
				table += String.format("%-27s",pcl.getDimensionsAsString());
				table += String.format("%-25.2f",pcl.getWeight());
				table += pcl.getDays() + "\r\n";
			}
			table += "\r\nTOTAL parcels uncollected:  " + finalUncollected.size() + "\r\n";
		}

		return table;
	}


	/**
	 *  sort uncollected parcels by number of days in depot
	 * @return  table of uncollected parcels in number of days order
	 */
	public String listByDays()
	{
		finalUncollected = hashMapToArray();
		Collections.sort(finalUncollected, new UncollectedParcelDaysComparator());
		return produceUncollectedTable(finalUncollected);
	}


	/**
	 * produces a report to be written to file
	 * inserts headings, etc. but gets content from other methods and classes
	 */
	public void compileReport() {

		String finalReport = "============== DETAILS OF COLLECTED AND UNCOLLECTED PARCELS IN THE DEPOT ==============\r\n\r\n";
		finalReport += "=============================== 1. UNCOLLECTED PARCELS ================================\r\n";
		finalReport += "======================== BY NO. OF DAYS IN DEPOT (DESCENDING) =========================\r\n\r\n";
		finalReport += "PARCEL ID                  DIMENSIONS             WEIGHT (KILOS)               DAYS\r\n\r\n";

		finalReport += listByDays();

		// sends report to be written to file
		writeToLog("report.txt",finalReport,"report 1");
	}


	/**
	 * sets value of DepotUtils.pclLoopFinished variable to true
	 * and notifies waiting threads (customer thread)
	 */
	public synchronized void setPclFinishedFlag() {

		// set flag
		DepotUtils.setPclLoopFinished(true);

		// notify any waiting threads
		notifyAll();
	} 


	/**
	 * step 2 of setting value of AutoGenrator.custLoopFinished to true
	 * check that pclLoopFinished=true
	 */
	public synchronized void setCustFinishedFlagPt2() {

		while(! DepotUtils.getPclLoopFinished()) {
			try { // if parcel thread not yet finished, wait
				wait();
			} catch (InterruptedException ie) {
			}
		}

	} 

	/**
	 * passes event log entry to TextWriter to be written to file
	 * @param filename  the filename to write to
	 * @param report  the text to write
	 * @param mode  information about the text, used by TextWriter
	 */
	public void writeToLog(String filename, String report, String mode) {
		TextWriter tw = TextWriter.getInstance();
		tw.writeToFile(filename, report, mode);
		eventLogEntry = "";
	}


	/**
	 * METHODS FOR DEPOTSUBJECT INTERFACE
	 */

	/**
	 * Adds a DepotObserver to the list of registered observers
	 * @param obs  the DepotObserver object
	 */
	public void registerObserver(DepotObserver obs) {
		registeredObservers.add(obs);	}

	/**
	 * Removes a DepotObserver from the list of registered observers
	 * @param obs  the DepotObserver object
	 */
	public void removeObserver(DepotObserver obs) {
		registeredObservers.remove(obs);	}

	/**
	 * Notifies all DepotObservers in the list of registered observers
	 * that a change to the data has occurred
	 * @param info  String information about the type of data change
	 */
	public void notifyObservers(String info) {
		for( DepotObserver obs : registeredObservers) {
			// call observer's update method to retrieve data
			obs.update(info);
		} 

	}

}
